import base64
import logging
import os
import re
import sys
import threading
import time
import traceback
from os.path import join, dirname
from urllib.parse import quote

import requests
import tornado.ioloop
from tornado.web import RequestHandler, StaticFileHandler, Application, Finish, HTTPError

import C
import utils
import R
import dao
import json
import poster
import store

domain = C.config['domain']


class BaseHandler(RequestHandler):

    def set_default_headers(self) -> None:
        origin = self.request.headers.get('Origin')
        origin = '*' if not origin else origin
        self.set_header('Access-Control-Allow-Methods', 'POST, PUT, DELETE, GET, OPTIONS')
        self.set_header('fastposter', 'fastposter-pro/v2.0.20')
        self.set_header('Access-Control-Allow-Credentials', 'true')
        self.set_header('Access-Control-Allow-Origin', origin)
        self.set_header('Access-Control-Allow-Headers', 'x-requested-with,token,Content-type')

    def options(self, *args: str, **kwargs: str):
        self.set_status(200)
        self.finish()

    def json(self, r: R = R.error()):
        self.set_header('Content-Type', 'application/json;charset=UTF-8')
        self.write(r.json())

    def user_agent(self):
        return self.request.headers['User-Agent'] if 'User-Agent' in self.request.headers else ''

    def ip(self):
        ip = self.request.remote_ip
        ip = '0.0.0.0' if not ip else ip
        return ip

    def _handle_request_exception(self, e):
        if isinstance(e, Finish):
            if not self._finished:
                self.finish(*e.args)
            return
        try:
            self.log_exception(*sys.exc_info())
        except Exception:
            logging.error("Error in exception logger", exc_info=True)
        if self._finished:
            return
        if isinstance(e, HTTPError):
            self.send_error(e.status_code, reason=e.reason)
        else:
            self.send_error(500, reason='ERROR')


class BaseAuthHandler(BaseHandler):

    def prepare(self):
        self.check_token()

    def token(self):
        t = self.request.headers['token'] if 'token' in self.request.headers else None
        if not t:
            t = self.get_argument('token', None)
        if not t:
            t = self.request.headers['Authorization'] if 'Authorization' in self.request.headers else None
            t = t.replace('Bearer ', '') if t and t.startswith("Bearer ") else t
        if not t:
            t = self.get_body_argument('token', None)
        return t

    def findToken(self, token):
        # ########################################################################
        # 通过token获取用户信息
        # 自定义部分
        ts = int(time.time() * 1000)
        url = f"http://{domain['domain']}/user/info?t={ts}"
        resp = requests.get(url, headers={'Authorization': token})
        """
        {
            'code': 0,
            'msg': '',
            'data': {
                'member_id': 12419,
                'is_vip': 1,
                'vip_days': 215,
                'nick_name': '',
                'username': '3716',
                'mobile': '15617973716',
                'vip_name': '企业版会员',
                'vip_id': 9,
                'end_time': 1728904083,
                'end_day': 215,
                'wx_is_bind': 0,
                'type': 1,
                'sub_id': '',
                'sub_status': 0,
                'master_mobile': '15617973716',
                'auth': 1,
                'province': '',
                'city': '',
                'district': ''
            }
        }
        """

        if resp.ok:
            mobile = resp.json()['data']['mobile']
            member_id = resp.json()['data']['member_id']
            jk_username = resp.json()['data']['username']
            vip_id = resp.json()['data']['vip_id']
            vip_name = resp.json()['data']['vip_name']
            end_time = resp.json()['data']['end_time']
            auth = resp.json()['data']['auth']
            vip_days = resp.json()['data']['vip_days']

            if mobile:
                mobile = 'dso_' + mobile
                print('获取到mobile:', mobile)
                u = dao.find_user(mobile)
                if not u:
                    u = dao.add_user(mobile, member_id, jk_username, vip_id, vip_name, end_time, auth, vip_days)
                    print(f"通过token自动创建用户: token={token}, mobile={mobile}, user={u}")
                tk = dao.query_token(token)
                if not tk:
                    # 新增token
                    tkid = dao.save_token(u['id'], token)
                    print(f"通过token自动创建TOKEN: token={token}, mobile={mobile}, tk={tkid}")
                return True

        return False

    def check_token(self):
        t = self.token()
        if not t:
            self.json(R.expire('not token'))
            return self.finish()
        if not dao.query_token(t):
            if self.findToken(t):
                return
            self.json(R.expire())
            return self.finish()

    def uid(self):
        t = self.token()
        if t:
            tk = dao.query_token(t)
            if tk:
                return int(tk[0])
        return None


class BaseAdminAuthHandler(BaseAuthHandler):

    def check_token(self):
        t = self.token()
        if not t:
            self.json(R.expire('not token'))
            return self.finish()
        if not dao.query_token(t):
            self.json(R.expire())
            return self.finish()
        user = dao.query_userinfo(self.uid())
        if not user:
            self.json(R.expire())
            return self.finish()
        if user.get('type', 0) != 2:
            self.json(R.forbidden())
            return self.finish()


class BasePageQueryHandler(BaseHandler):

    def get_page_params(self):
        page = int(self.get_argument('page', '1', True))
        limit = int(self.get_argument('limit', '10', True))
        start = (page - 1) * limit
        return {
            '_start': start,
            '_limit': limit
        }


class ActuatorHealthHandler(BaseHandler):

    def get(self):
        self.set_header('Content-Type', 'application/json;charset=UTF-8')
        self.write(json.dumps({'status': 'UP'}))


class ApiLoginHandler(BaseHandler):

    def post(self):
        d = json.loads(self.request.body)
        self.json(dao.login(d))


class ApiLogoutHandler(BaseAuthHandler):

    def post(self):
        self.json(dao.logout(self.token()))


class ApiChangePwdHandler(BaseAuthHandler):

    def post(self):
        uid = self.uid()
        user = dao.query_userinfo(uid)
        d = json.loads(self.request.body)
        if d['password'] != user['password']:
            return self.json(R.error("原密码不正确"))
        pwd = d['passwordNew']
        if not pwd or len(pwd) < 6:
            return self.json(R.error('新密码至少6位'))
        return self.json(dao.changepwd(uid, pwd))


class ApiUserHandler(BaseAuthHandler):

    def get(self):
        user = dao.query_userinfo(self.uid())
        print('用户信息', user)
        del user['password']
        self.json(R.ok().add('user', user))


class ApiAdminUserHandler(BaseAdminAuthHandler, BasePageQueryHandler):

    def get(self, *args: str, **kwargs: str):
        print('args', args)
        if args and len(args) == 1:
            uid = args[0]
            return self.json(dao.admin_query_user(uid))
        else:
            d = self.get_page_params()
            d['keyword'] = self.get_argument('keyword', '', True)
            d['status'] = self.get_argument('status', '', True)
            self.json(dao.admin_query_users(d))

    def put(self):
        user = json.loads(self.request.body)
        self.json(dao.admin_update_user(user))

    def post(self):
        user = json.loads(self.request.body)
        self.json(dao.admin_add_user(user))

    def delete(self, uid):
        self.json(dao.admin_delete_user(uid))


class ApiTokenHandler(BaseAuthHandler):

    def get(self):
        self.json(R.ok().add('list', dao.query_user_tokens(self.uid())))

    def put(self):
        d = json.loads(self.request.body)
        d['uid'] = self.uid()
        self.json(dao.update_user_token(d))


class ApiPostersHandler(BaseAuthHandler):

    def get(self, *args: str, **kwargs: str):
        id = args[0] if len(args) == 1 else None
        if id:
            poster = dao.query_user_poster(self.uid(), id)
            self.json(R.ok().add('poster', poster))
        else:
            posters = dao.query_user_posters(self.uid())
            self.json(R.ok().add('posters', posters))

    def delete(self, id):
        uid = self.uid()
        self.json(dao.db_delete_poster(uid, int(id)))

    def build_preview(self, d):
        buf, mimetype = poster.drawio(d, 0.4)
        return store.save(buf.getvalue(), f"preview.{d['type']}", 'preview')

    def post(self):
        d = json.loads(self.request.body)
        d['uuid'] = C.code()
        d['preview'] = self.build_preview(d)
        id = dao.save_user_poster(self.uid(), d)
        poster = dao.query_user_poster(self.uid(), id)
        self.json(R.ok().add('poster', poster))

    def put(self):
        d = json.loads(self.request.body)
        d['preview'] = self.build_preview(d)
        dao.update_user_poster(self.uid(), d)
        self.json(R.ok())


class ApiMaterialsHandler(BaseAuthHandler):

    def get(self, *args: str, **kwargs: str):
        param = {
            'name': self.get_argument('name'),
            'uid': self.uid()
        }
        items = dao.query_materials(param)
        self.json(R.ok().add('items', items))

    def delete(self, id):
        uid = self.uid()
        self.json(dao.db_delete_material(uid, int(id)))


def batchBuildThread(batch):
    """
    批量生成后台线程
    """
    batchId = batch['id']
    uid = batch['uid']
    posterId = batch['posterId']
    name = batch['name']
    p = dao.query_user_poster(uid, posterId)
    type = batch['type']
    raw = json.loads(batch['raw'])
    num = batch['num']
    if C.inpublic():
        num = min(num, 10)  # 演示环境，最多只批量生成10张
    try:
        # 生成前先清空目录
        batch_path = f'{C.STORE_DIR}/batch/{batchId}'
        store.DiskStore().clearn(f'{batch_path}/')
        outpath = f'{batch_path}.zip'
        if os.path.exists(outpath):
            os.remove(outpath)
            print('删除')

        for i in range(0, num):
            print(f'batchId={batchId}, build:{i + 1}')
            param = raw[i]
            d = json.loads(p['json'])
            for item in d['items']:
                vd = item['vd'].strip()
                if vd:
                    if not param.get(vd, None):
                        continue
                    v = param[vd]
                    if v:
                        item['v'] = str(v).strip()
            # 处理背景图片
            if param.get('bgUrl', None):
                d['bgUrl'] = param['bgUrl']
            d['type'] = type
            buf, mimetype = poster.drawio(d)
            path = f'batch/{batchId}/{name}-%04d.{type}' % (i + 1)
            store.DiskStore().save(buf.getvalue(), path)
            dao.update_batch_process(i + 1, batchId)

        # 压缩目录
        utils.zipDir(batch_path, outpath)
        print(f'(压缩目录)batchId={batchId}, outpath={outpath}')

        # 上传到云存储
        with open(outpath, mode='rb') as f:
            path = store.save(f.read(), outpath)
            dao.update_batch_process_done(2, None, path, batchId)
            print(f'(打包上传)batchId={batchId}, path={path}')
    except Exception as e:
        traceback.print_exc()
        s = traceback.format_exc()
        dao.update_batch_process_done(3, s, '', batchId)
        pass


class ApiBatchsHandler(BaseAuthHandler):

    def get(self, *args: str, **kwargs: str):
        id = args[0] if len(args) == 1 else None
        if id:
            batch = dao.query_batch(self.uid(), id)
            self.json(R.ok().add('batch', batch))
        else:
            items = dao.query_batchs(self.uid())
            self.json(R.ok().add('items', items))

    def post(self):
        d = json.loads(self.request.body)
        print(int(time.time()), d)
        d['status'] = 1
        id = dao.save_batch(d, self.uid())
        d['id'] = id
        d['uid'] = self.uid()
        thread = threading.Thread(target=batchBuildThread, args=(d,))
        thread.start()
        self.json(R.ok().add('batch', {'id': id}))

    def put(self):
        d = json.loads(self.request.body)
        d['status'] = 1
        d['uid'] = self.uid()
        dao.update_batch(d, self.uid())
        thread = threading.Thread(target=batchBuildThread, args=(d,))
        thread.start()
        self.json(R.ok().add('batch', {}))

    def delete(self, id):
        uid = self.uid()
        self.json(dao.delete_batch(uid, int(id)))


class ApiFontHandler(BaseAuthHandler):

    def get(self, *args: str, **kwargs: str):
        fonts = dao.query_user_fonts(self.uid())
        self.json(R.ok().add('fonts', fonts))


class ApiQrcodeHandler(BaseHandler):

    def get(self, *args: str, **kwargs: str):
        c = tornado.web.escape.url_unescape(self.request.uri)
        c = c.replace("/api/qr/", "")
        data = json.loads('''{
    "w": 500,
    "h": 500,
    "use": "bgUrl",
    "bgc": "#fafbfc",
    "bgUrl": "",
    "type": "jpeg",
    "quality": 80,
    "items": [
        {
            "t": "qrcode",
            "name": "二维码",
            "uuid": "aQLrkQpH",
            "x": 0,
            "y": 0,
            "w": 500,
            "h": 500,
            "z": 1,
            "s": 30,
            "c": "#000000",
            "bgc": "#ffffff",
            "v": "%s",
            "vd": "",
            "fn": "",
            "st": 0,
            "active": false,
            "lock": false,
            "p": 0,
            "angle": 0
        }
    ]
}''' % (c))
        buf, mimetype = poster.drawio(data)
        self.set_header('Content-Type', mimetype)
        self.write(buf.getvalue())


class ApiPostersCopyHandler(BaseAuthHandler):

    def post(self, id):
        uid = self.uid();
        poster = dao.query_user_poster(uid, id)
        if not poster:
            return self.json(R.error('没有找到海报'))
        poster['uuid'] = C.code()
        dao.copy_user_poster(uid, poster)
        return self.json(R.ok())


class ApiPreviewHandler(BaseHandler):

    def post(self):
        data = json.loads(self.request.body)
        buf, mimetype = poster.drawio(data)
        self.set_header('Content-Type', mimetype)
        self.write(buf.getvalue())


class ApiUploadHandler(BaseAuthHandler):

    def post(self):
        uid = self.uid()
        files = []
        for field_name, fs in self.request.files.items():
            for f in fs:
                filename, body, content_type = f["filename"], f['body'], f["content_type"]
                size = len(body)
                checksum = C.md5(body)
                dbf = dao.find_store(uid, checksum)
                if dbf:
                    files.append(dbf)
                else:
                    # 执行保存
                    path = store.save(body, filename)
                    # 获取图片宽高
                    width, height = poster.getImageSize(body)
                    id = dao.save_store((uid, filename, path, size, checksum, content_type))
                    files.append({
                        'id': id,
                        'filename': filename,
                        'path': path,
                        'width': width,
                        'height': height
                    })
        self.json(R.ok().add("files", files))


class ApiLinkHandler(BaseAuthHandler):

    def post(self):
        param = json.loads(self.request.body)
        code = C.md5(param, 16)
        if not dao.find_link(code):
            id = param['id']
            uid = self.uid()
            if not dao.query_user_poster(uid, id):
                return self.json(R.error('poster not exists.'))
            dao.save_link([uid, id, code, self.request.body])
        url = f"{uri}/v/{code}".replace('//v', '/v')
        if self.user_agent().startswith('curl'):
            return self.write(url + "\n")
        self.json(R.ok().add('url', url))


class ApiViewHandler(BaseHandler):

    def get(self, opt: str, code: str):
        scale = float(self.request.arguments.get('scale', [1])[0])
        c = code.split('.')
        poster_id, link_id, uid, d = dao.find_link_param(c[0])
        if not d:
            return self.json(R.error('no poster here!'))
        if len(c) == 2 and c[1] in ('png', 'pdf', 'webp', 'jpg', 'jpeg'):
            d['type'] = c[1]
        buf, mimetype = poster.drawio(d, scale)
        if opt == 'd':
            self.set_header('content-disposition', 'attachment')
        if opt == 'dn':
            n = quote(f"{d['name']}.{d['type']}")
            # 解决Safari下载海报中文乱码问题
            # "filename*" 采用了  RFC 5987 中规定的编码方式
            # https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
            self.set_header('content-disposition', f"attachment;filename*=UTF-8''{n}")
        if len(c) == 2 and c[1].startswith('b64'):
            b64 = base64.b64encode(buf.read()).decode()
            self.write(b64)
        else:
            self.set_header('Content-Type', mimetype)
            self.write(buf.getvalue())


class ApiBuildPosterHandler(BaseAuthHandler):

    def post(self):
        args = json.loads(self.request.body)
        print(args)
        traceId = C.code(32)
        payload = args['payload']  # type: str
        if not payload.startswith("{"):
            # 需要base64解码
            payload = base64.b64decode(payload)
        data = dao.find_build_data(args['uuid'], json.loads(payload))
        if data is None:
            print('no poster here!')
            self.write(R.error('no poster here!'))
            return
        buf, mimetype = poster.drawio(data)
        self.set_header('fastposter-traceid', traceId)
        if args.get('b64', False):
            b64 = base64.b64encode(buf.read()).decode()
            self.write(b64)
        else:
            self.set_header('Content-Type', mimetype)
            self.write(buf.getvalue())


# 解决跨域问题
class MyStaticFileHandler(StaticFileHandler, BaseHandler):
    pass


def make_app(p):
    # path = "static" if C.indocker() else "../design/dist"
    path = "static"
    settings = {
        'debug': not C.indocker() or os.environ.get('POSTER_DEBUG', 'false') == 'true'
    }
    return Application([
        (f"{p}actuator/health", ActuatorHealthHandler),

        (f"{p}api/login", ApiLoginHandler),
        (f"{p}api/logout", ApiLogoutHandler),
        (f"{p}api/changepwd", ApiChangePwdHandler),
        (f"{p}api/user", ApiUserHandler),

        (f"{p}api/admin/users", ApiAdminUserHandler),
        (f"{p}api/admin/users/(.+)", ApiAdminUserHandler),

        (f"{p}api/posters", ApiPostersHandler),
        (f"{p}api/materials", ApiMaterialsHandler),
        (f"{p}api/materials/(.+)", ApiMaterialsHandler),
        (f"{p}api/batchs", ApiBatchsHandler),
        (f"{p}api/batchs/(.+)", ApiBatchsHandler),
        (f"{p}api/posters/(.+)/copy", ApiPostersCopyHandler),
        (f"{p}api/posters/(.+)", ApiPostersHandler),
        (f"{p}api/preview", ApiPreviewHandler),
        (f"{p}api/upload", ApiUploadHandler),
        (f"{p}api/tokens", ApiTokenHandler),

        (f"{p}api/fonts", ApiFontHandler),
        (f"{p}api/qr/(.+)$", ApiQrcodeHandler),

        (f"{p}api/link", ApiLinkHandler),
        (f"{p}v1/build/poster", ApiBuildPosterHandler),

        (f"{p}(v|d|dn)/(.+)", ApiViewHandler),
        (f'{p}(store/.*)$', MyStaticFileHandler, {"path": join(dirname(__file__), "data")}),
        (f'{p}(.*)$', StaticFileHandler, {"path": join(dirname(__file__), path), "default_filename": "index.html"})

    ], **settings)


if __name__ == "__main__":
    PORT = int(os.environ.get('POSTER_PORT', '7780'))
    FORK = os.environ.get('FASTPOSTER_FORK', 'false') == 'true'
    uri = os.environ.get('POSTER_URI_PREFIX', f'http://0.0.0.0:{PORT}/')
    g = re.search(r'http[s]?://.*?(/.*)', uri)
    web_context_path = '/' if not g else g.group(1)
    app = make_app(web_context_path)
    if FORK:
        print('Listening at ', uri, ' in fork mode.')
        server = tornado.web.HTTPServer(app)
        server.bind(PORT, address='0.0.0.0')
        server.start(num_processes=None)
    else:
        app.listen(port=PORT, address='0.0.0.0')
        print('Listening at ', uri)
    tornado.ioloop.IOLoop.current().start()
